在部分系统模块中 (例如 fs
),你可能会奇怪为什么有 fs.readFile
,fs.readFileSync
,fs.promises.readFile
3 种方式。
实际上对应着 3 种编码方式,最初只有同步 (sync) 和异步回调 (callback) 两种方式,在 ES6 (ES2015) 推出之后才有的 async/await
和 Promise
。
同步操作:同步操作会阻塞当前线程,直到操作完成。因此,在执行耗时操作时,应避免使用同步操作方式,因为它可能导致应用程序的响应性变差;
异步回调函数 (Callbacks):最初的异步执行依托于回调函数实现,回调函数是一个作为参数传递给另一个函数的函数,当异步操作完成时,回调函数会被调用,但当多层嵌套的时候会导致回调地狱;
异步方法 (Async/Await):ES6 引入的新语法可让我们更方便地写异步代码,并且避免了回调地狱。
下面举一些实际的例子来帮助理解。
下面是最常见的同步方法,
js
function hello(){
console.log('hello world')
}
代码会一行行的执行,不会跳着执行。
js
console.log(1)
hello()
console.log(2)
通常在带有异步方法的情况下,内容输出时机与异步方法执行时间有关,
比如下面的打印 123
。
```js const { readFile } = require('fs')
console.log(1) readFile(__filename, () => { console.log(3) }) console.log(2) ```
如果要输出 132
咱们只能使用 readFileSync
方法,
那么如何实现一个类似的 Sync 方法呢?我们以 fetch 为例,实现一个 fetchSync,
可以结合使用 child_process.spawnSync
创建子进程来实现一个简单的 fetchSync。
```js const { spawnSync } = require('child_process')
function fetchSync(url, options = {}) { // 将请求参数和 URL 传递给子进程 const child = spawnSync(process.argv[0], [ './_fetch-sync.js', JSON.stringify(url), JSON.stringify(options) ])
// 子进程的标准输出即为请求结果 const result = child.stdout.toString() const responseData = JSON.parse(result) return responseData.body } ```
子进程代码如下 _fetch-sync.js
。
```js // 如果当前文件是被子进程执行的 if (process.argv.length === 4) { // 从子进程的参数中获取请求参数 const url = JSON.parse(process.argv[2]) const options = JSON.parse(process.argv[3])
fetch(url, options).then(async (response) => { // 将响应数据发送给父进程 const result = { status: response.status, body: await response.json() } console.log(JSON.stringify(result)) }) } ```
运行结果如下。
js
console.log(1)
console.log(
fetchSync(
'https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot&count=3&from=1&aid=2608&uuid=7145810834685003271&spider=0'
)
)
console.log(2)
读取文件的同时,可以执行其它代码,
```js import fs from 'fs'
console.log(1) fs.readFile('./index.mjs', (err, data) => { console.log(2, data.toString()) }) console.log(3) ```
但如果在回调里还要执行异步的回调就会有回调地狱的问题,下面是一个例子。
比如要顺序读取 3 个文件,使用回调的方式就需要像下面这样书写,非常的不优雅。
```js import fs from 'fs'
const fileA = 'index.mjs' const fileB = 'index.mjs' const fileC = 'index.mjs' fs.readFile(fileA, (err, dataA) => { console.log('File A content:')
fs.readFile(fileB, (err, dataB) => { console.log('File B content:')
fs.readFile(fileC, (err, dataC) => {
console.log('File C content:')
})
}) }) ```
在实际复杂的业务场景中会导致难以维护代码,
下面看看 async/await 如何解决这个问题。
利用 async/await
配合 Promise 可以很优雅的处理异步代码。
```js import fs from 'fs/promises'
const fileA = 'index.mjs' const fileB = 'index.mjs' const fileC = 'index.mjs' async function main() { const dataA = await fs.readFile(fileA) console.log('File A content:') const dataB = await fs.readFile(fileB) console.log('File B content:') const dataC = await fs.readFile(fileC) console.log('File C content:') }
main() ```
本节介绍了同步方法、异步回调函数、async/await 三种编码方式的异同:
在介绍同步方法时,介绍了如何利用 child_process.spawnSync
实现一个同步方法 fetchSync
,
在介绍异步回调函数时,介绍了回调地狱的问题,
最后通过 async/await 优雅的编写异步调用代码。